MariaDB기반 MultiDomain
목차
MariaDB기반 Multi Domain #
파일시스템기반 multi domain설정을 mariaDB기반 multi domain설정으로 변경하는 과정을 기록 했습니다. 누락된 정보가 있을 수 있습니다. ai에게 도움을 받으면 부족한 부분을 충분히 채울 수 있으리라 생각 합니다. 여기까지 오는데 ai의 도움을 많이 받았습니다. 초보 분들이 메일서버 설정에서 ai와 무한 루프에 빠지는 일이 없기를 바라는 마음으로 기록 남깁니다. 도움이 되기를 바랍니다.
이 글을 읽기 전에
대략적인 구조입니다 #
Postfix Mysql 구조
Postfix
│
├─ mysql-virtual-domains.cf ──► virtual_domains
│
├─ mysql-virtual-users.cf ──► virtual_users
│
├─ mysql-virtual-aliases.cf ──► virtual_aliases
│
├─ mysql-sender-canonical.cf ──► sender_canonical
│
└─ mysql-virtual-sender-maps.cf ──► virtual_users
MariaDB
/ | \
virtual_domains
virtual_users
virtual_aliases
sender_canonical
│
│
Postfix
│
│ LMTP
│
Dovecot
│
│
Maildir
각 .cf 파일은 Postfix가 “이 조건일 때 MariaDB에 어떤 쿼리를 날려서
답을 받아올지"를 정의하는 연결 설정입니다. Postfix 자체는 DB를 직접
모르고, 이 파일들을 통해서만 MariaDB와 대화합니다.
새로 생성하거나 수정할 파일목록입니다 #
sudo nano /etc/dovecot/conf.d/10-ssl.conf
sudo nano /etc/dovecot/dovecot-sql.conf.ext
sudo nano /etc/postfix/mysql-sender-canonical.cf
sudo nano /etc/postfix/mysql-virtual-domains.cf
sudo nano /etc/postfix/mysql-virtual-users.cf
sudo nano /etc/postfix/mysql-virtual-aliases.cf
sudo nano /etc/postfix/mysql-virtual-sender-maps.cf
sudo nano /etc/postfix/master.cf
sudo nano /etc/postfix/main.cf
sudo nano /etc/dovecot/dovecot.conf
sudo nano /etc/dovecot/conf.d/10-mail.conf
sudo nano /etc/dovecot/conf.d/10-master.conf
sudo nano /etc/dovecot/conf.d/20-lmtp.conf
sudo nano /etc/dovecot/conf.d/10-auth.conf
sudo nano /etc/mysql/mariadb.conf.d/50-server.cnf
기본 시스템 및 MariaDB 준비 #
MariaDB 설치
sudo apt update
sudo apt install mariadb-server mariadb-client -y
데이터베이스의 자물쇠 설치 (초기 보안 설정 마법사)
sudo mysql_secure_installation
- 처음 설치 시 첫 항목 그냥 enter
Switch to unix_socket authentication?→ nChange root password?→ y (password 입력)Remove anonymous users?→ yDisallow root login remotely?→ yRemove test database?→ yReload privilege tables now?→ y
Postfix에게 주소록 연결을 위한 설치
sudo apt install postfix-mysql -y
Dovecot에게 사용자 정보 연결을 위한 설치
sudo apt install dovecot-mysql -y
메일을 전송하는 효율적인 통로 설치 (LMTP)
sudo apt install dovecot-lmtpd -y
MariaDB 시작
sudo systemctl daemon-reload
sudo systemctl enable mariadb
sudo systemctl start mariadb
sudo systemctl status mariadb
메일 전용 DB 및 계정 생성 #
Postfix/Dovecot은 조회(SELECT)만 하면 되므로 mailuser는 읽기
권한만, 관리 스크립트나 웹 UI에서 쓰는 mailadmin은 쓰기 권한까지
분리해서 부여합니다. 이렇게 권한을 나눠두면 메일 처리 경로 쪽
코드에 문제가 생겨도 DB 내용이 함부로 바뀌지 않습니다.
CREATE DATABASE mailserver CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'mailuser'@'127.0.0.1' IDENTIFIED BY 'password';
GRANT SELECT ON mailserver.* TO 'mailuser'@'127.0.0.1';
CREATE USER 'mailuser'@'localhost' IDENTIFIED BY 'password';
GRANT SELECT ON mailserver.* TO 'mailuser'@'localhost';
CREATE USER 'mailadmin'@'localhost' IDENTIFIED BY 'password';
GRANT SELECT, INSERT, UPDATE, DELETE ON mailserver.* TO 'mailadmin'@'localhost';
FLUSH PRIVILEGES;
DB 접속 후 확인
mariadb -u root -p
USE mailserver;
-- 현재 어떤 계정으로 접속했는지 확인
SELECT USER(), CURRENT_USER();
-- mailserver DB가 실제로 생성됐는지 확인
SHOW DATABASES LIKE 'mailserver';
-- 전체 DB 계정 목록
SELECT Host, User FROM mysql.user;
-- mailadmin 계정만 확인
SELECT Host, User FROM mysql.user WHERE User = 'mailadmin';
-- mailadmin에게 부여된 권한 확인
SHOW GRANTS FOR 'mailadmin'@'localhost';
-- 인증 방식(plugin) 확인
SELECT Host, User, plugin, authentication_string FROM mysql.user;
테이블 구조 설계 #
CREATE TABLE virtual_domains (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE,
active TINYINT(1) NOT NULL DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
CREATE TABLE virtual_users (
id INT AUTO_INCREMENT PRIMARY KEY,
domain_id INT NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
quota BIGINT NOT NULL DEFAULT 0,
active TINYINT(1) NOT NULL DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
KEY idx_domain_active (domain_id, active),
KEY idx_email_active (email, active),
CONSTRAINT fk_users_domain
FOREIGN KEY (domain_id) REFERENCES virtual_domains(id)
ON DELETE CASCADE
) ENGINE=InnoDB;
CREATE TABLE virtual_aliases (
id INT AUTO_INCREMENT PRIMARY KEY,
domain_id INT NOT NULL,
source VARCHAR(255) NOT NULL,
destination VARCHAR(255) NOT NULL,
active TINYINT(1) NOT NULL DEFAULT 1,
KEY idx_source_active (source, active),
KEY idx_domain (domain_id),
KEY idx_destination (destination),
CONSTRAINT fk_alias_domain
FOREIGN KEY (domain_id) REFERENCES virtual_domains(id)
ON DELETE CASCADE
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS sender_canonical (
id INT AUTO_INCREMENT PRIMARY KEY,
source VARCHAR(255) NOT NULL,
destination VARCHAR(255) NOT NULL,
active TINYINT(1) NOT NULL DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY unique_source (source),
KEY idx_active (active)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
active 컬럼으로 계정/도메인 활성화 관리 가능. (삭제 대신
active=0으로 바꾸면 이력이 남아서 복구가 쉽습니다.)
sender_canonical 테이블은 “이 주소로 보낸 것처럼 보이게 바꿔치기”
하는 용도입니다. 예를 들어 서버 내부 프로세스(root, cron 등)가
자동으로 보내는 메일의 발신 주소를 실제 관리자 메일 주소로 통일하는
데 씁니다. 아래 마이그레이션 단계에서 실제 예시가 나옵니다.
기존 파일 데이터 DB 마이그레이션 #
도메인 입력
INSERT INTO virtual_domains(name, active) VALUES
('domain1.com', 1),
('domain2.com', 1),
('domain3.com', 1),
('domain4.com', 1),
('domain5.com', 1),
('domain6.com', 1),
('domain7.com', 1),
('domain8.com', 1);
도메인 전체 목록 확인
SELECT * FROM virtual_domains;
특정 도메인이 활성화 상태인지 확인
SELECT name FROM virtual_domains WHERE name='domain1.com' AND active=1;
사용자 입력 (Dovecot의 SHA512-CRYPT 해시를 그대로 사용)
INSERT INTO virtual_users (domain_id, email, password) VALUES
(1, 'user1@domain1.com', '{SHA512-CRYPT}hash1'),
(1, 'user2@domain1.com', '{SHA512-CRYPT}hash2'),
(2, 'user1@domain2.com', '{SHA512-CRYPT}hash3'),
(2, 'user2@domain2.com', '{SHA512-CRYPT}hash4'),
(3, 'user1@domain3.com', '{SHA512-CRYPT}hash5'),
(3, 'user2@domain3.com', '{SHA512-CRYPT}hash6'),
(4, 'user1@domain4.com', '{SHA512-CRYPT}hash7'),
(4, 'user2@domain4.com', '{SHA512-CRYPT}hash8')
/* ... */;
시스템 계정인 root가 메일을 발송하면, 외부에는 master@domain1.com이 보낸 것으로 표시됩니다.
INSERT INTO sender_canonical (source, destination, active)
VALUES
('root', 'master@domain1.com', 1),
('root@domain1.com', 'master@domain1.com', 1);
시스템 내부에서 자동으로 생성되는 메일들의 “보낸 사람” 주소가 master@domain1.com로 바뀝니다.
INSERT INTO sender_canonical (source, destination, active)
VALUES
('www-data', 'master@domain1.com', 1),
('postfix', 'master@domain1.com', 1),
('vmail', 'master@domain1.com', 1),
('cron', 'master@domain1.com', 1);
INSERT INTO virtual_aliases (domain_id, source, destination) VALUES
(1, 'info@domain1.com', 'master@domain1.com'),
(1, 'admin@domain1.com', 'master@domain1.com'),
(1, 'webmaster@domain1.com', 'master@domain1.com'),
(1, 'postmaster@domain1.com', 'dmarc@domain1.com'),
(2, 'info@domain2.com', 'master@domain2.com'),
(3, 'info@domain3.com', 'master@domain3.com'),
(4, 'info@domain4.com', 'master@domain4.com'),
(5, 'info@domain5.com', 'master@domain5.com'),
(6, 'info@domain6.com', 'master@domain6.com'),
(7, 'info@domain7.com', 'master@domain7.com'),
(8, 'info@domain8.com', 'master@domain8.com');
전체를 리스트로 보고 싶을 때 사용합니다.
SELECT * FROM virtual_aliases;
\G 옵션을 사용하면 각 행을 세로로 깔끔하게 나열해 줍니다.
SELECT * FROM virtual_aliases\G
SELECT DISTINCT source FROM virtual_aliases ORDER BY source;
특정 도메인의 별칭만 확인하기
SELECT * FROM virtual_aliases
WHERE source LIKE '%@domain1.com';
SELECT * FROM sender_canonical;
SELECT * FROM sender_canonical WHERE source = 'vmail';
단독 조회 테스트 (postmap으로 실제 Postfix가 보는 것과 동일한 방식으로 쿼리 결과 확인)
sudo postmap -q root mysql:/etc/postfix/mysql-sender-canonical.cf
sudo postmap -q root@domain1.com mysql:/etc/postfix/mysql-sender-canonical.cf
sudo postmap -q nobody mysql:/etc/postfix/mysql-sender-canonical.cf
Postfix + MariaDB 연동 #
MariaDB용 Postfix CF 파일 생성
sudo nano /etc/postfix/mysql-virtual-domains.cf
user = mailuser
password = password
hosts = 127.0.0.1
dbname = mailserver
query = SELECT 1 FROM virtual_domains WHERE name='%s' AND active=1
sudo nano /etc/postfix/mysql-virtual-users.cf
user = mailuser
password = password
hosts = 127.0.0.1
dbname = mailserver
query = SELECT 1 FROM virtual_users WHERE email='%s' AND active=1
sudo nano /etc/postfix/mysql-virtual-aliases.cf
user = mailuser
password = password
hosts = 127.0.0.1
dbname = mailserver
query = SELECT destination FROM virtual_aliases WHERE source='%s' AND active=1
sudo nano /etc/postfix/mysql-virtual-sender-maps.cf
user = mailuser
password = password
hosts = 127.0.0.1
dbname = mailserver
query = SELECT email FROM virtual_users WHERE email='%s' AND active=1
sudo nano /etc/postfix/mysql-sender-canonical.cf
user = mailuser
password = password
hosts = 127.0.0.1
dbname = mailserver
query = SELECT destination FROM sender_canonical WHERE source='%s' AND active=1
Dovecot + MariaDB 연동 #
sudo nano /etc/dovecot/dovecot-sql.conf.ext
driver = mysql
connect = host=127.0.0.1 dbname=mailserver user=mailuser password=password
default_pass_scheme = SHA512-CRYPT
password_query = \
SELECT email as user, password \
FROM virtual_users \
WHERE email='%u' AND active=1
user_query = \
SELECT '/var/mail/vhosts/%d/%n' as home, 1005 as uid, 1005 as gid \
FROM virtual_users \
WHERE email='%u' AND active=1
권한 설정 #
Postfix/Dovecot이 읽어야 하는 설정 파일에는 비밀번호가 평문으로 들어있으므로, 해당 서비스 그룹만 읽을 수 있게 권한을 좁혀둡니다.
sudo chown root:postfix /etc/postfix/mysql-virtual-*.cf
sudo chmod 640 /etc/postfix/mysql-virtual-*.cf
sudo chown root:postfix /etc/postfix/mysql-sender-canonical.cf
sudo chmod 640 /etc/postfix/mysql-sender-canonical.cf
sudo chown root:dovecot /etc/dovecot/dovecot-sql.conf.ext
sudo chmod 640 /etc/dovecot/dovecot-sql.conf.ext
Postfix에 방금 만든 연동 파일들을 등록합니다.
sudo postconf -e "sender_canonical_maps = mysql:/etc/postfix/mysql-sender-canonical.cf"
sudo postconf -e "virtual_mailbox_domains = mysql:/etc/postfix/mysql-virtual-domains.cf"
sudo postconf -e "virtual_mailbox_maps = mysql:/etc/postfix/mysql-virtual-users.cf"
sudo postconf -e "virtual_alias_maps = mysql:/etc/postfix/mysql-virtual-aliases.cf"
sudo postconf -e "smtpd_sender_login_maps = mysql:/etc/postfix/mysql-virtual-sender-maps.cf"
sudo postconf -e "sender_canonical_classes = envelope_sender,header_sender"
Dovecot을 통해 인증하도록 설정 추가 후 테스트 #
SASL(Simple Authentication and Security Layer)은 “로그인 인증을 어떤 방식으로 처리할지"에 대한 표준 규격입니다. 여기서는 Postfix가 자체적으로 인증하지 않고, Dovecot에게 인증을 위임하도록 설정합니다 (Dovecot이 이미 DB 연동을 갖추고 있으니 중복으로 만들 필요가 없습니다).
sudo postconf -e "smtpd_sasl_type = dovecot"
sudo postconf -e "smtpd_sasl_path = private/auth"
sudo postconf -e "smtpd_sasl_auth_enable = yes"
sudo postconf -e "smtpd_sasl_security_options = noanonymous"
Test
sudo postmap -q domain1.com mysql:/etc/postfix/mysql-virtual-domains.cf
sudo postmap -q master@domain1.com mysql:/etc/postfix/mysql-virtual-users.cf
sudo postmap -q master@domain1.com mysql:/etc/postfix/mysql-virtual-sender-maps.cf
virtual_domains→1virtual_users→1virtual_sender_maps→email
결과값이 1 → 1 → master@domain1.com 순서로 출력되면 성공입니다.
10-auth.conf 수정 #
sudo nano /etc/dovecot/conf.d/10-auth.conf
disable_plaintext_auth = no
auth_mechanisms = plain login
!include auth-sql.conf.ext
auth-client 소켓 설정 #
sudo nano /etc/dovecot/conf.d/10-master.conf
service anvil {
client_limit = 2000
}
service auth {
client_limit = 2000
process_limit = 1
unix_listener /var/spool/postfix/private/auth {
mode = 0660
user = postfix
group = postfix
}
unix_listener auth-userdb {
mode = 0660
user = vmail
group = vmail
}
}
# Dovecot Tuning
service imap-login {
process_limit = 500
client_limit = 1000
}
# LMTP Socket Setting
service lmtp {
unix_listener /var/spool/postfix/private/dovecot-lmtp {
mode = 0660
user = postfix
group = postfix
}
}
anvil은 Dovecot 내부에서 “동시 접속 수, 접속 빈도” 같은 걸 집계해서
로그인 폭주나 무차별 대입 시도를 막는 역할을 하는 보조 프로세스입니다.
client_limit은 이 프로세스가 동시에 처리할 수 있는 연결 수 상한이고,
도메인/계정 수가 많으면 기본값으로는 부족할 수 있어서 넉넉하게
늘려둔 값입니다.
/etc/dovecot/dovecot.conf 설정 #
sudo nano /etc/dovecot/dovecot.conf
mail_max_userip_connections = 50
protocols = imap lmtp
auth_cache_size = 50M
auth_cache_ttl = 2 hours
ssl = required
auth_cache_ttl은 한 번 인증에 성공한 결과를 얼마나 오래 캐시해둘지
정하는 값입니다. 계정을 비활성화했다가 다시 활성화해도 이 캐시
시간 동안은 예전 인증 결과가 남아있을 수 있으니, 급하게 반영해야
할 때는 doveadm auth cache flush 명령으로 직접 비워줘야 합니다.
/etc/postfix/master.cf 설정 #
sudo nano /etc/postfix/master.cf
smtp inet n - n - - smtpd
submission inet n - n - - smtpd
submissions inet n - n - - smtpd
smtp unix - - n - - smtp
proxymap unix - - n - - proxymap
cleanup unix n - n - 0 cleanup
rewrite unix - - n - - trivial-rewrite
local unix - n n - - local
lmtp unix - - n - - lmtp
/etc/dovecot/conf.d/10-mail.conf 설정 #
sudo nano /etc/dovecot/conf.d/10-mail.conf
# mail_location = maildir:/var/mail/vhosts/%d/%n
# INBOX NAMESPACE
namespace inbox {
type = private
separator = /
prefix =
location = maildir:/var/mail/vhosts/%d/%n
inbox = yes
mailbox Drafts {
special_use = \Drafts
auto = subscribe
}
mailbox Junk {
special_use = \Junk
auto = subscribe
}
mailbox Sent {
special_use = \Sent
auto = subscribe
}
mailbox Trash {
special_use = \Trash
auto = subscribe
}
}
maildir_stat_dirs = yes
maildir_copy_with_hardlinks = yes
mail_privileged_group = mail
mail_access_groups = mail
lock_method = fcntl
/etc/dovecot/conf.d/20-lmtp.conf 설정 #
sudo nano /etc/dovecot/conf.d/20-lmtp.conf
protocol lmtp {
postmaster_address = master@domain1.com
mail_plugins = $mail_plugins
}
/etc/mysql/mariadb.conf.d/50-server.cnf 설정 #
MariaDB 성능 튜닝
sudo nano /etc/mysql/mariadb.conf.d/50-server.cnf
[mysqld]
bind-address = 127.0.0.1
# innodb_buffer_pool_size는 서버 RAM 8GB 기준 2G로 설정 (RAM 용량이 다르면 그에 맞게 조정)
innodb_buffer_pool_size = 2G
innodb_log_file_size = 256M
innodb_flush_method = O_DIRECT
innodb_flush_log_at_trx_commit = 2
# MariaDB connection
max_connections = 500
thread_cache_size = 100
table_open_cache = 2000
# slow query (monitoring)
slow_query_log = 1
slow_query_log_file = /var/log/mysql/mariadb-slow.log
long_query_time = 2
/etc/postfix/main.cf 설정 #
Postfix 동시 처리량
sudo nano /etc/postfix/main.cf
default_process_limit = 200
smtp_destination_concurrency_limit = 20
smtp_destination_rate_delay = 1s
minimal_backoff_time = 300s
maximal_backoff_time = 4000s
queue_run_delay = 300s
smtpd_client_connection_count_limit = 20
smtpd_client_connection_rate_limit = 30
virtual_transport = lmtp:unix:private/dovecot-lmtp
마지막 점검 #
모든 디렉토리를 700으로 (vmail 유저만 접근 가능)
sudo chown -R vmail:vmail /var/mail/vhosts
sudo find /var/mail/vhosts -type d -exec chmod 700 {} +
모든 파일을 600으로 (불필요한 실행 권한 제거)
sudo find /var/mail/vhosts -type f -exec chmod 600 {} +
소켓 파일이 존재하는지 확인 (srw------- 타입)
sudo ls -l /var/spool/postfix/private/dovecot-lmtp
Dovecot이 관리하는 프로토콜 목록 확인
sudo doveconf protocols
결과에 imap lmtp가 포함되어야 함
서비스 재시작 및 점검 #
sudo postfix check
sudo systemctl restart mariadb
sudo systemctl restart dovecot
sudo systemctl restart postfix
DB에러시 확인 #
sudo journalctl -xeu mariadb.service | tail -n 20
# MariaDB 설정 파일 문법 검증 (버전에 따라 이 옵션이 없을 수 있으니,
# 실패하면 아래처럼 dry-run으로 기동 여부만 확인하는 방법도 있습니다)
sudo mariadbd --validate-config
# 대안: sudo mariadbd --help --verbose 로 지원 옵션 목록 확인
sudo doveadm auth test user1@domain1.com
ls -l /var/run/dovecot/auth-userdb
sudo ls -l /var/spool/postfix/private/auth
sudo ls -l /var/spool/postfix/private/dovecot-lmtp
sudo ls -l /var/run/dovecot/auth-client
파일시스템 점검 #
sudo nano /etc/fstab
/dev/disk/by-id/dm-uuid-*** / ext4 defaults,noatime,nodiratime 0 1
/dev/disk/by-uuid/*** /boot ext4 defaults,noatime 0 2
/swap.img none swap sw 0 0
절대 바로 재부팅하지 마세요
sudo mount -a
아무런 메시지도 나오지 않아야 정상입니다.
만약 에러가 난다면 즉시 /etc/fstab을 다시 열어 오타를 수정하세요.
mount | grep -E '/ |/boot'
/dev/mapper/ubuntu--vg-ubuntu--lv on / type ext4 (rw,noatime,nodiratime)
/dev/sda2 on /boot type ext4 (rw,noatime)
적용이 안 됐다면
sudo mount -o remount,noatime /boot
마지막 확인 #
설정 파일 내에서 ssl_cert와 ssl_key 항목 확인
grep -E "ssl_cert|ssl_key" /etc/dovecot/conf.d/10-ssl.conf
Postfix hostname (중요) — 이거 안 맞으면 메일 헤더, TLS, spam score 전부 꼬일 수 있음
sudo nano /etc/hostname
mail.domain1.com
sudo nano /etc/hosts
127.0.0.1 localhost
203.0.113.10 mail.domain1.com mail
sudo nano /etc/postfix/main.cf
myhostname = mail.domain1.com
mydomain = domain1.com
myorigin = $mydomain
결과
hostname
# mail.domain1.com
hostname -f
# mail.domain1.com
postconf myhostname
# mail.domain1.com
인증서 외 #
sudo nano /etc/postfix/main.cf
smtpd_tls_security_level = may
smtp_tls_security_level = may
mydestination = localhost
# Postfix 기본값이지만 명시하는 게 안전 — 없으면 queue 권한 문제가 가끔 발생함
virtual_transport = lmtp:unix:private/dovecot-lmtp
mail_owner = postfix
# 메일 호스팅에서는 기본값(약 50MB)이 문제됨 — 0은 무제한
mailbox_size_limit = 0
# 대용량 첨부 대비 (50MB)
message_size_limit = 52428800
Dovecot auth socket 권한 확인 필수
sudo ls -l /var/spool/postfix/private/auth
정상 예시: srw-rw---- postfix postfix
틀리면 SASL authentication failure 발생함